iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
佛心分享-SideProject30

30 天開發一款真正能每天使用的散步 App系列 第 9

基礎散步流程(5)——繪製路線簡圖

  • 分享至 

  • xImage
  •  

今天要把自由散步的基本流程收尾。

如何將路線繪製成簡圖?

最簡單的做法,是在完成頁再放一個 MapView,並使用 Polyline 顯示路線,也就是前面顯示散步軌跡時使用的方式:

<MapView>
  <Polyline coordinates={coordinates} />
</MapView>

但如果只是要顯示一條靜態路線,重新載入完整的地圖元件就有點多餘。

因此,我決定改用 react-native-svg 繪製路線簡圖,直接將 GPS 點位轉換成 SVG 座標,再畫成一張靜態路線縮圖。

整體流程如下:

GPS 點位
   ↓
找出最大與最小經緯度
   ↓
計算路線範圍
   ↓
修正經度比例
   ↓
計算共用縮放值
   ↓
計算置中偏移
   ↓
轉換成 SVG 座標
   ↓
使用 Polyline 繪製

找出最大與最小經緯度

GPS 點位使用的是經緯度:

{
  latitude: 25.033,
  longitude: 121.5654,
}

SVG 使用的則是畫布座標:

<Svg viewBox="0 0 100 100">

因此,需要先確認整條路線在經度和緯度方向上的分布範圍。

首先取出所有點位的經緯度:

const latitudes = coordinates.map(
  point => point.latitude,
);

const longitudes = coordinates.map(
  point => point.longitude,
);

接著找出最大值與最小值:

const minLat = Math.min(...latitudes);
const maxLat = Math.max(...latitudes);

const minLng = Math.min(...longitudes);
const maxLng = Math.max(...longitudes);

這四個值就代表整條路線的邊界。

計算路線範圍

取得最大值與最小值後,就能計算路線在垂直方向跨了多少緯度:

const latRange = maxLat - minLat;

例如:

maxLat = 25.040
minLat = 25.030

latRange = 0.010

代表這條路線在緯度方向橫跨了 0.01°

經度方向也可以先用相同方式計算:

const lngRange = maxLng - minLng;

不過,經度和緯度雖然都是用度表示,實際代表的距離卻不完全相同,因此 lngRange 還不能直接拿來和 latRange 比較。

經度與緯度不能直接使用相同比例

一度緯度在地球上代表的距離大致固定,而一度經度則會隨著所在緯度改變,越接近兩極就越短。

例如在台灣,大約位於北緯 22°~25°,一度經度大約是 101~103 公里;如果是在赤道附近,則大約是 111 公里。

如果直接把經度差與緯度差當成相同比例處理,畫出的路線就可能產生變形。

一度經度的實際距離,可以近似看成一度緯度乘上 cos(緯度),因此這裡使用整條路線的平均緯度,計算經度方向的縮放比例:

const centerLat = (minLat + maxLat) / 2;

const longitudeScale = Math.cos(
  centerLat * Math.PI / 180,
);

longitudeScale 代表目前緯度下一度經度,相較於赤道還剩下多少比例。

例如在北緯 25°:

cos(25°) ≈ 0.906

代表一度經度的實際距離,大約是一度緯度的 90.6%

Math.cos() 接收的是弧度,而 GPS 提供的緯度是角度,因此需要先將角度轉成弧度,也就是乘上 Math.PI / 180

接著將原本的經度範圍乘上修正比例:

const adjustedLngRange =
  lngRange * longitudeScale;

現在兩個方向的範圍分別是:

const latRange = maxLat - minLat;

const lngRange = maxLng - minLng;

const adjustedLngRange =
  lngRange * longitudeScale;

其中:

  • latRange:垂直方向的路線範圍
  • adjustedLngRange:修正後的水平方向路線範圍

這樣才能正確比較整條路線是偏寬還是偏高。

水平與垂直方向必須共用同一個縮放值

完成經度修正後,接著要決定整條路線要放大到什麼程度。

如果水平和垂直方向分別縮放到填滿畫布,原本很小的水平或垂直偏移就可能被過度放大,導致路線失去原本的形狀。

因此,水平與垂直方向必須共用同一個縮放比例:

const viewBoxSize = 100;
const drawableSize = 84;

const routeRange = Math.max(
  latRange,
  adjustedLngRange,
);

const scale = drawableSize / routeRange;

routeRange 會取水平與垂直範圍中較大的那一個。

例如路線比較高:

latRange = 0.010
adjustedLngRange = 0.004

就會使用 0.010 作為縮放基準。

這樣較大的方向會使用完整的可繪製空間,較小的方向則按照相同比例縮放,因此能保留路線原本的長寬比例。

這裡將 drawableSize 設為 84,也就是在 100 × 100 的畫布中預留一部分空間,避免路線和起終點貼到邊緣。

不過,由於路線會等比例縮放,較短的方向不一定會剛好使用完整的 84 個單位,因此還要另外計算水平與垂直方向的置中偏移:

const actualSvgWidth =
  adjustedLngRange * scale;

const actualSvgHeight =
  latRange * scale;

const offsetX =
  (viewBoxSize - actualSvgWidth) / 2;

const offsetY =
  (viewBoxSize - actualSvgHeight) / 2;

剩餘空間會平均分配到路線兩側,讓路線不論是偏寬還是偏高,都能顯示在畫布中央。

處理 SVG 的 Y 軸方向

SVG 的座標原點位於左上角,y 值越大,位置就越往下。

但經緯度在轉成地圖位置時,緯度越大代表位置越北,顯示在畫面上時也應該越靠上方。

如果直接以最南邊的 minLat 作為基準:

point.latitude - minLat

緯度越大的點,算出的值也會越大,結果越北的點反而會被畫到越下面,整條路線就會上下顛倒。

因此,需要改成以最北邊的 maxLat 作為基準:

maxLat - point.latitude

這樣最北的點會得到最小的 y 值,最南的點則會得到最大的 y 值,繪製方向才會和地圖一致。

實際轉換成 SVG 座標

完成前面的計算後,就可以將每一個 GPS 點位轉成 SVG 座標:

const svgPoints = coordinates.map(point => {
  const x =
    offsetX +
    (point.longitude - minLng) *
      longitudeScale *
      scale;

  const y =
    offsetY +
    (maxLat - point.latitude) *
      scale;

  return { x, y };
});

x 座標的計算流程是:

目前經度與最小經度的差
× 經度修正比例
× 畫布縮放比例
+ 水平置中偏移

y 座標則是:

最大緯度與目前緯度的差
× 畫布縮放比例
+ 垂直置中偏移

這樣不管路線是偏寬、偏高,還是接近正方形,都能在保留原本比例的情況下顯示在畫布中央。

完成轉換後,原本的 GPS 點位:

{
  latitude: 25.033,
  longitude: 121.5654,
}

就會變成 SVG 可以使用的座標:

{
  x: 42.7,
  y: 35.6,
}

接著將所有轉換後的點位組成 Polyline 所需的座標字串:

const path = svgPoints
  .map(point => `${point.x},${point.y}`)
  .join(' ');

產生的結果會類似:

8,92 24.5,68.2 42.7,35.6 76.1,8

最後交給 SVG 繪製:

<Polyline
  points={path}
  fill="none"
  stroke="#34322E"
  strokeWidth={1.8}
  strokeLinecap="round"
  strokeLinejoin="round"
/>

另外再使用兩個圓形標記第一個點與最後一個點,就能讓使用者分辨路線的起點與終點。

完成自由散步功能

完成路線縮圖後,自由散步的基本流程也算告一段落了。

開始散步
    ↓
持續接收 GPS 點位
    ↓
過濾並暫存點位
    ↓
批次同步到後端
    ↓
暫停與繼續
    ↓
結束散步
    ↓
後端計算正式統計
    ↓
顯示統計與路線縮圖

到這裡,使用者已經可以完整走完一次自由散步。

從開始記錄 GPS 點位,到過濾、同步與結束散步,整個流程都已經串起來。散步完成後,也能看到距離、時間、速度與路線縮圖,基本功能就算完整了。


上一篇
基礎散步流程(4)——完成散步統計
下一篇
推薦路線(1)——根據散步需求生成路線
系列文
30 天開發一款真正能每天使用的散步 App12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言